Skip to content

Explicit Resource Management for pg pool client#3661

Open
hyperair wants to merge 5 commits into
brianc:masterfrom
hyperair:disposable-pool-client
Open

Explicit Resource Management for pg pool client#3661
hyperair wants to merge 5 commits into
brianc:masterfrom
hyperair:disposable-pool-client

Conversation

@hyperair
Copy link
Copy Markdown
Contributor

@hyperair hyperair commented Apr 27, 2026

Add support for Explicit Resource Management for pg.Pool clients.

This is implemented using [Symbol.dispose] instead of [Symbol.asyncDispose] because client.release() is a synchronous function, not async.

Additionally, add a second property client.destroyOnDispose so that Symbol.dispose supports calling client.release(true). This is useful for a usecase that changes connection parameters in a way that is troublesome to reset upon release to the pool, e.g.

const pool = new Pool({
    database: 'foo',
    user: 'foo',
    password: 'foo',
    port: 5432,

    statement_timeout: 10_000,
});

async function getSlowClient() {
    const client = await pool.connect();
    await client.query("SET statement_timeout = 0");
    client.destroyOnDispose = true;
}

async function performReallySlowQuery() {
    using client = await getSlowClient();

    client.query(`select pg_sleep_for('30 minutes')`);
}

Fixes: #3515

Note: This PR has been rebased on top of #3662, so there are some irrelevant commits until that PR is merged.

@hyperair hyperair force-pushed the disposable-pool-client branch 3 times, most recently from bac8c73 to 7d69534 Compare April 27, 2026 05:05
@hyperair
Copy link
Copy Markdown
Contributor Author

Hmm, looks like eslint needs to be upgraded before it'll parse the using test

@hyperair hyperair force-pushed the disposable-pool-client branch from 7d69534 to 0cb5f67 Compare April 27, 2026 08:42
@hyperair hyperair marked this pull request as draft April 27, 2026 13:10
@hyperair hyperair force-pushed the disposable-pool-client branch from 0cb5f67 to 24caf46 Compare April 28, 2026 10:26
@hyperair hyperair marked this pull request as ready for review April 28, 2026 10:34
@hyperair hyperair requested a review from hjr3 as a code owner April 28, 2026 10:34
Copy link
Copy Markdown
Collaborator

@charmander charmander left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about destroyOnDispose, i.e. whether its job is common enough to justify it and whether it’s the best API for the job. Would suggest initializing to false if kept. Note .release(true) continues to work for error cases given that double release is a no-op; I recognize that it doesn’t satisfy the use case you mentioned. Maybe release the feature without that API initially, since it can be added easily?

Edit: Sorry, I don’t know where I got that from – we haven’t even done PooledClient yet.

Comment thread packages/pg-pool/test/disposable-clients.js Outdated
Comment thread packages/pg-pool/index.js

client.release = this._releaseOnce(client, idleListener)

if (Symbol.dispose) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would normally go on the prototype, but pg-pool’s approach is already a bit messy (it’s lacking a PooledClient object that’s specific to one acquisition operation) so it’s also not the end of the world if the patch goes in as-is.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and that's why I implemented it like this.

@types/pg has a PooledClient type with the release signature, and I intend to make a PR there to add the [Symbol.dispose] addition later.

Comment thread docs/pages/apis/pool.mdx Outdated
@hyperair
Copy link
Copy Markdown
Contributor Author

Not sure about destroyOnDispose, i.e. whether its job is common enough to justify it and whether it’s the best API for the job. Would suggest initializing to false if kept. Note .release(true) continues to work for error cases given that double release is a no-op; I recognize that it doesn’t satisfy the use case you mentioned. Maybe release the feature without that API initially, since it can be added easily?

Yeah I wasn't sure about the API either but I couldn't think of a better way to do it. Maybe a client.setError() to set an error state that client.release() acknowledges as well?

That said, unfortunately I don't think relying on double-release is workable either, because the second release, which will be called from within Symbol.dispose will throw. Even if [Symbol.dispose]() wraps that in a try-catch to swallow the error, needing to put the client.release(true) in a finally clause obviates any purpose of using the using syntax.

@hyperair hyperair force-pushed the disposable-pool-client branch from 36ef0cf to 8c8d974 Compare April 30, 2026 04:21
@olsonpm
Copy link
Copy Markdown

olsonpm commented May 2, 2026

I got here because I'm implementing dispose on a client wrapper.

One question, why implement destroyOnDispose as a mutable property instead of an internal one initialized by pool.connect()? e.g.

using client = await pool.connect({ destroyOnDispose: true });

Maybe I'm not understanding the use case you mention.

@hyperair
Copy link
Copy Markdown
Contributor Author

hyperair commented May 2, 2026

@olsonpm That does work for my usecase, I just didn't think of it.

But on the other hand it's still a shift from how late you can make the decision not to return the client to the pool -- with client.release(true), you're able to make this decision all the way at the end of your session (starting from pool.connect until client.release) with this client, but with a pool.connect parameter, you need to commit to destroying the client all the way at the beginning of your session, before you even have the client instance.

An example where it might be desirable to destroy the client rather than release it back to the pool would be if you made a client.query and caught an error that indicated that the client was no longer usable. You might then want to set destroyOnDispose = true instead of letting the client go back to the pool.

@olsonpm
Copy link
Copy Markdown

olsonpm commented May 2, 2026

Ah thanks for that use case Hyperair. I was aware the decision would change the semantics as you mentioned, but couldn't think of why you would want that flexibility.

hyperair and others added 5 commits May 11, 2026 11:24
When `Symbol.dispose` is defined, define a disposer function that simply calls
`this.release()`. This lets the `PoolClient` with the `using` syntax work with any
downstream-overridden `release` functions.

This makes `PoolClient` support Explicit Resource Management when the runtime
supports it.

Fixes: brianc#3515
Add a `destroyOnDispose` property on the client to signal that
`[Symbol.dispose]()` should call `client.release(true)` instead of
`client.release()`.
Co-authored-by: Charmander <~@charmander.me>
`connect` has two `n`s, and we don't like semicolons

Co-authored-by: Charmander <~@charmander.me>
@charmander charmander force-pushed the disposable-pool-client branch from 8c8d974 to f56d448 Compare May 11, 2026 18:28
@brianc
Copy link
Copy Markdown
Owner

brianc commented May 11, 2026

Hey thanks for this! I actually set out to do this a while ago and due to the linting and it not working on older versions of node I grew frustrated and thought I'd do it later. And now it's done!

I'm also not sold on destroyOnDispose though...it is kinda a weird API to me. I can't really think of a better way to handle it though. I found it kinda frustrating actually in the dispose ecmascript design there's no way to know if an error was thrown in the scope or if the scope exited "naturally." I took a stab at having a using client.transaction() type thing but not being able to issue a rollback on error was a show stopper.

One possible alternative is to just allow ended clients to be returned to the pool (via the disposal or via client.release()) and if the pooled client has had 'end' called then it is evicted automatically from the pool. This would prevent likely error scenarios of misuse anyway where one accidentally returns an ended client (obviously not recommended, but could happen) and then the code would look like this

using client = await pool.connect()
try {
  client.query("EXPLODE!")
} catch (e) {
  client.end()
  throw e // (or just log it if you want or whatever)
}

Thoughts on that?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Symbol.asyncDispose support

4 participants